跳到主要内容

Vue组件、插槽、Ref

组件化规范

Web Components 通过创建封装好功能的定制元素解决上述问题

  1. 尽可能多的重用代码
  2. 自定义组件的方式不太容易(html、css和js)
  3. 多次使用组件可能导致冲突

一些创建规则:

  1. data需要使用一个函数来返回对象
  2. 组件模板内容必须是单个 根元素(只能有一个根元素,内部可以写其他元素)

全局组件注册

语法

Vue.component(组件名称, {
data: 组件数据,
template: 组件模板内容
})

例如:

// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
data: function(){
return {
count: 0
}
},
template: "<button @click='count++'>点击了{{count}}次</button>"
})

上面直接写在点击事件里的语句,其实可以通过调用函数来完成

// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
data: function(){
return {
count: 0
}
},
template: "<button @click='handle'>点击了{{count}}次</button>",
methods: {
handle: function(){
this.count++
}
}
})

使用

<button-counter></button-counter>
<!-- 不同的组件,相互之间的数据是独立的 -->
<button-counter></button-counter>

局部组件注册

局部组件只能在注册它的父组件里使用

let componentA = {
data: function () {
return {
count: 0
}
},
template: `
<div>
<button @click='count++'>点击了{{count}}次</button>
<button>测试</botton>
</div>
`
}

let vm = new Vue({
el: '#app',
components: {
'component-a': componentA
}
})

组件里的 data

为什么在vue的组件中,data要用function返回对象呢?

在创建或注册模板的时候,传入一个data属性作为用来绑定的数据。但是在组件中,data必须是一个函数,而不能直接把一个对象赋值给它。这是vm实例和组件的最大区别

Vue.component('my-component', {
template: '<div>OK</div>',
data() {
return {} // 返回一个唯一的对象,不要和其他组件共用一个对象进行返回
},
})

当一个组件被定义, data 必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例。如果 data 仍然是一个纯粹的对象,则所有的实例将共享引用同一个数据对象!通过提供 data 函数,每次创建一个新实例后,我们能够调用 data 函数,从而返回初始数据的一个全新副本数据对象

模板字符串

因为直接在template里通过文本写html可读性比较差,所以使用 模板字符串 (就是 ` 符号) 这个是ES6的新特性,优点就是可以换行和使用 ${ }

Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: `
<div>
<button @click='count++'>点击了{{count}}次</button>
<button>测试</botton>
</div>
`
})

x-template 模板

就是以 # 开始,则它将被用作选择符,并使用匹配元素的 innerHTML 作为模板。常用的技巧是用 <script type="x-template"> 包含模板。

好处就是不用写字符串了(字符串无法整理代码)

<script type="text/x-template" id="my-component">
<div>这是组件的内容</div>
</script>

组件的命名方式

短横线方式

Vue.component('my-component',{ /* ... */})

驼峰式方式(少用)

Vue.component('MyComponent',{ /* ... */})

注意:如果使用驼峰式命名组件,那么在使用组件的时候,只能在字符串模板中使用驼峰式命名,而不能直接写在页面上,因为 dom元素是不区分大小写的,所以会自动把命名转化成小写 例如MyComponent会被转成mycomponent

这样Vue就无法找到这个组件了,因此尽量使用短横线的方式

组件的使用

因为不同的组件就是不同的实例,所以无法直接传值,需要通过其他的方法接收到外部的数据

  • 父组件向子组件传值:通过属性来传值
  • 子组件向父组件传值:通过自定义事件来传值

父组件向子组件传值

组件内部通过props接收传递过来的值

Vue.component('menu-item',{
props: ['title'],
template: '<div>{{title}}</div>'
})

父组件通过属性将值传递给子组件

<menu-item :title="title"></menu-item>

例子:

这个例子中的父组件指的是Vue的实例对象,也就是#app 其中子组件先在 props 里声明参数传进来的入口属性叫啥 然后子组件再 通过props里定义的属性 来接收父组件的值

<div id="app">
<div>{{pmsg}}</div>
<!-- 注意这里要使用 v-bind来绑定属性,否则只能传递静态写死的值 -->
<menu-item :title='pmsg'></menu-item>
</div>
// 子组件
Vue.component('menu-item', {
props: ['title'],
data: function () {
return {
msg: '子组件本身的数据'
}
},
template: `<div>{{msg + '-------' + title}}</div>`
})

// 父组件
let vm = new Vue({
el: '#app',
data: {
pmsg: '父组件中的内容'
}
})

遍历父组件数组

<div id="app">
<!-- 父组件传值给子组件通过属性 -->
<test-a :list='list'></test-a>
</div>
Vue.component('test-a', {
props:['list'],
template: `
<div>
<li v-for="item in list" :key="item.id">{{item.name}}</li>
</div>
`
})

let vm = new Vue({
el: '#app',
data: {
list: [
{ id: 1, name: 'apple' },
{ id: 2, name: 'orange' },
{ id: 3, name: 'banana' }
]
}
})

$emit 和自定义事件

官方文档--emit 官方文档--components-custom-events

/**
*
* @param {string} eventName
* @param {[...args]} param1
*/
vm.$emit( eventName, […args] )

作用:触发当前实例上的事件。附加参数都会传给监听器回调。

所谓的自定义事件,实际上就是对一些已有的操作进行封装,例如下面这个例子就算是自定义了一个叫做 welcome 的事件

原理就是在自组件中当触发click之后通过$emit发射一个welcome事件已经触发的信号 给外部绑定了welcome事件的v-on

v-on实际上就是绑定了vm.$on( event, callback ) v-on 官方文档

$on:监听当前实例上的自定义事件。事件可以由 vm.$emit 触发。回调函数会接收所有传入事件触发函数的额外参数。

示例:

Vue.component('welcome-button', {
template: `
<button v-on:click="$emit('welcome')">
Click me to be welcomed
</button>
`
})

绑定这个事件,使事件触发时执行sayHi函数

<div id="emit-example-simple">
<welcome-button @welcome="sayHi"></welcome-button>
</div>
new Vue({
el: '#emit-example-simple',
methods: {
sayHi: function () {
alert('Hi!')
}
}
})

子组件向父组件传值

流程如下:

子组件通过自定义事件向父组件传递信息

<button @click='$emit("enlarge-text")'>扩大字体</button>

父组件监听子组件的事件

<menu-item @enlarge-text='fontSize += 0.1'></menu-item>

例子:

这里通过v-on监听自定义事件enlarge-text是否触发

<div id="app">
<div :style='{fontSize: fontSize + "px"}'>HELLO WORLD</div>
<!-- 让自定义事件绑定 handle 函数 -->
<menu-item :parr='parr' @enlarge-text='handle'></menu-item>
</div>

这里就是 通过$emit 发射一个自定义事件已经触发的消息 来告诉外部绑定的 v-on 事件已经触发

如下自定义的事件enlarge-text实际就是封装了click

Vue.component('menu-item', {
props: ['parr'],
template: `
<div>
<ul>
<li v-for="(item, index) in parr" :key="index">{{item}}</li>
</ul>
<button @click='parr.push("lemon")'>点击添加柠檬</button>
<button @click='$emit("enlarge-text")'>扩大父组件中字体大小</button>
</div>`
})

let vm = new Vue({
el: '#app',
data: {
parr: ['apple', 'orange', 'banana'],
fontSize: 10
}, methods: {
handle: function () {
// 扩大字体大小
this.fontSize += 5
}
},
})

非父子组件传值

原理就是通过一个专门的事件中心来管理组件间的通信

U7Ul34.png

  1. 单独的事件中心管理组件间的通信
var eventHub = new Vue()
  1. 监听事件与销毁事件
eventHub.$on('add-todo',addTodo)
eventHub.$off('add-todo')

其中销毁事件$off,参考自官方文档

作用:移除自定义事件监听器。 就是使 $on 不再监听该事件 用法:

  • 如果没有提供参数,则移除所有的事件监听器;
  • 如果只提供了事件,则移除该事件所有的监听器;
  • 如果同时提供了事件与回调,则只移除这个回调的监听器。
  1. 触发事件
eventHub.$emit('add-todo',id)

例子

<div id="app">
<!-- 按下这个销毁事件,两个事件都无法传输了 -->
<button @click="handle">销毁事件</button>
<a-tom></a-tom>
<b-tom></b-tom>
</div>
// 创建一个事件中心来管理事件
let hub = new Vue()

// 组件A
Vue.component('a-tom', {
data: function () {
return {
num: 0
}
},
template: `
<div>
<div>A:{{num}}</div>
<div>
<button @click='handle'>点击后发射给B</button>
</div>
</div>`,
methods: {
handle: function () {
// 通过这个函数来发射事件(告诉对面的接收器,当前已经触发了这个事件)
hub.$emit('b-event', 2)
}
},
mounted: function () {
// 通过钩子函数来监听事件(因为这个钩子函数在实例化好后,对象时就会执行)
hub.$on('a-event', (val) => {
this.num += val
})
}
})

// 组件B
Vue.component('b-tom', {
data: function () {
return {
num: 0
}
},
template: `
<div>
<div>B:{{num}}</div>
<div>
<button @click='handle'>点击后发射给A</button>
</div>
</div>`,
methods: {
handle: function () {
// 通过这个函数来发射事件
hub.$emit('a-event', 1)
}
},
mounted: function () {
// 通过钩子函数来监听事件(因为这个钩子函数在实例化好后,对象时就会执行)
hub.$on('b-event', (val) => {
this.num += val
})
}
})


// 这个不能省略,因为需要一个Vue实例绑定一个节点,上面的操作都是全局的
let vm = new Vue({
el: '#app',
methods:{
handle:function(){
// 销毁事件
hub.$off('a-event')
hub.$off('b-event')
}
}
})

props 属性名规则

这个和上面组件名的命名规则一样,驼峰式也有那些坑 所以尽量使用短横线的形式

同样:字符串形式的模板没有这个限制

插槽slot

插槽基本使用

参考官网插槽

组件插槽的作用 就是父组件向子组件传递内容

U77ShT.png

插槽的位置:就是直接丢<slot>标签到模板里面

Vue.component('a-tom', {
template: `
<div>
<slot></slot>
</div>`
})

插槽的内容就是一些写在标签之间的信息,这个信息可以是模板代码,或者html等等 例如下面的这个插入的就是 Hello slot

<h1>Hello slot</h1>

插槽里也可以填充一些默认内容,例如下面这个,当模板中间没有填入内容的话,默认就是显示 this is slot

<slot>this is slot</slot>

具名插槽

参考自文档

例如:如果直接使用多个普通插槽,那么内容还是重复的

<div id="app">
<test-a>hello slot</test-a>
</div>
Vue.component('test-a', {
template: `
<div>
<slot></slot>
<slot></slot>
</div>`
})

结果如图,就只是单纯的重复了一遍而已

U7xJVe.png

所以当模板里需要使用到内容各不相同的插槽时,就可以使用具名插槽 元素有一个特殊的 attribute:name。这个属性可以用来定义额外的插槽:

<div class="container">
<header>
<!-- 我们希望把页头放这里 -->
<slot name="header"></slot>
</header>
<main>
<!-- 我们希望把主要内容放这里 -->
<slot></slot>
</main>
<footer>
<!-- 我们希望把页脚放这里 -->
<slot name="footer"></slot>
</footer>
</div>

具名插槽的用法也有一些不同,因为是通过属性来标识的,所以传值时也需要指定属性

在向具名插槽提供内容的时候,在一个 template 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称:

<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>

<p>A paragraph for the main content.</p>
<p>And another one.</p>

<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>

注意:旧版的是直接使用 slot 新版的属性名变成了 v-slot ,且 v-slot 只能添加在 template 上,旧版的可以写在任意的标签上

作用域插槽

参考自官方文档 参考资料 深入理解vue中的slot与slot-scope

应用场景:父组件对子组件的内容进行加工处理(就是父组件中可以获取到子组件的数据)

插槽内容

<!-- 父组件 -->
<fruit-list :list="list">
<!-- slot-scope 可以得到插槽定义绑定的属性(:item="item") -->
<template slot-scope="slotProps">
<!-- strong 标签是用来高亮显示的 -->
<strong v-if="slotProps.info.id==2">
{{slotProps.info.name}}
</strong>
<!-- 否则不高亮 -->
<span v-else>{{slotProps.info.name}}</span>
</template>
</fruit-list>

插槽定义

Vue.component('fruit-list', {
props: ['list'],
template: `
<ul>
<li v-for="item in list" :key="item.id">
<slot :info="item">
{{item.name}}
</slot>
</li>
</ul>
`,
data: function () {
return {
list: [{
id: 1,
name: 'apple'
},{
id: 2,
name: 'orange'
},{
id: 3,
name: 'banana'
}]
}
},
})

Ref 获取 DOM 或 组件

参考资料 Vue教程( ref 和 $refs 的使用)

在 Vue 中一般很少会用到直接操作 DOM,但不可避免有时候需要用到,这时我们可以通过 ref$refs 这两个来实现($refs 是一个对象,持有已注册过 ref 的所有的子组件)

ref 有三种用法:

  • ref 加在普通的元素上,用 this.$refs.name 获取到的是dom元素。
  • ref 加在子组件上,用 this.ref.name 获取到的是组件实例,可以使用组件的所有方法。
  • 利用 v-forref 获取一组数组或者 DOM 节点。
<div id="app">
<input type="button" value="获取h3的值" @click="getElement()">
<h3 id="myh3" ref="myh3" >我是一个h3</h3>
<hr>
<login ref='mylogin'></login>
</div>

<script>
var login = {
template: "<h3>我是login子组件</h3>",
data(){
return {
msg: "ok"
}
},
methods:{
show(){ console.log("show方法执行了...") }
}
}
var vm = new Vue({
el: "#app",
data: {},
methods: {
getElement(){
// 通过 getElementById 方式获取 DOM 对象
// console.log(document.getElementById("myh3").innerHTML)
// console.log(this.$refs.myh3.innerHTML)
console.log(this.$refs.mylogin.msg)
this.$refs.mylogin.show()
}
},
components:{
login
}
})
</script>

devtools 调试工具

devtools 是官方的一个调试工具

github--devtools 官网讲的有点复杂了,实际就是下载一个Chrome插件而已,可以直接在下面的谷歌商店下载安装 参考的中文教程 安装的Chrome插件

视频教程参考 黑马教程